﻿//AutoProtectiveStrategy.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo.Strategies
{
	using System;
	using System.Collections.Generic;
	using System.Linq;

	using Ecng.Collections;
	using Ecng.Common;

	using StockSharp.Algo.Positions;
	using StockSharp.BusinessEntities;
	using StockSharp.Logging;

	/// <summary>
	/// Стратегия автоматической защиты позиции.
	/// </summary>
	/// <remarks>
	/// Новые сделки в стратегию поступают через <see cref="ProcessNewMyTrades"/>.
	/// Они автоматически защищаются с помощью <see cref="TakeProfitStopLossStrategy"/>.
	/// Дополнительно, <see cref="AutoProtectiveStrategy"/> переворачивает стопы в случае переворота позиции.
	/// </remarks>
	public class AutoProtectiveStrategy : Strategy
	{
		private readonly SynchronizedDictionary<Security, IPositionManager> _positionManagers = new SynchronizedDictionary<Security, IPositionManager>();

		/// <summary>
		/// Создать <see cref="AutoProtectiveStrategy"/>.
		/// </summary>
		public AutoProtectiveStrategy()
		{
			_takeProfitLevel = this.Param("TakeProfitLevel", new Unit());
			_stopLossLevel = this.Param("StopLossLevel", new Unit());
			_stopLossActivationOffset = this.Param("StopLossActivationOffset", new Unit());
			_isTrailingStopLoss = this.Param<bool>("IsTrailingStopLoss");
			_isTrailingTakeProfit = this.Param<bool>("IsTrailingTakeProfit");
			_takeProfitTimeOut = this.Param<TimeSpan>("TakeProfitTimeOut");
			_stopLossTimeOut = this.Param<TimeSpan>("StopLossTimeOut");
		}

		private readonly StrategyParam<Unit> _takeProfitLevel;

		/// <summary>
		/// Защитный уровень для тейк-профита. По-умолчанию уровень равен 0, что означает отключено.
		/// </summary>
		public Unit TakeProfitLevel
		{
			get { return _takeProfitLevel.Value; }
			set { _takeProfitLevel.Value = value; }
		}

		private readonly StrategyParam<Unit> _stopLossLevel;

		/// <summary>
		/// Защитный уровень для стоп-лосса. По-умолчанию уровень равен 0, что означает отключено.
		/// </summary>
		public Unit StopLossLevel
		{
			get { return _stopLossLevel.Value; }
			set { _stopLossLevel.Value = value; }
		}

		private readonly StrategyParam<Unit> _stopLossActivationOffset;

		/// <summary>
		/// Защитный сдвиг от расcчитанной цены стопа <see cref="ProtectiveStrategy.ActivationPrice"/>, который позволяет активировать защиту до достижения рынком данной цены.
		/// По-умолчанию выключено.
		/// </summary>
		public Unit StopLossActivationOffset
		{
			get { return _stopLossActivationOffset.Value; }
			set { _stopLossActivationOffset.Value = value; }
		}

		private readonly StrategyParam<bool> _isTrailingStopLoss;

		/// <summary>
		/// Использовать ли механизм скольжения для <see cref="StopLossStrategy"/>. По-умолчанию выключено.
		/// </summary>
		public bool IsTrailingStopLoss
		{
			get { return _isTrailingStopLoss.Value; }
			set
			{
				if (value && StopLossLevel.Type == UnitTypes.Limit)
					throw new ArgumentException("Механизм скольжения не может быть использовать при лимитированном уровне стопа.");

				_isTrailingStopLoss.Value = value;
			}
		}

		private readonly StrategyParam<bool> _isTrailingTakeProfit;

		/// <summary>
		/// Использовать ли механизм скольжения для <see cref="TakeProfitStrategy"/>. По-умолчанию выключено.
		/// </summary>
		public bool IsTrailingTakeProfit
		{
			get { return _isTrailingTakeProfit.Value; }
			set
			{
				if (value && TakeProfitLevel.Type == UnitTypes.Limit)
					throw new ArgumentException("Механизм скольжения не может быть использовать при лимитированном уровне тейка.");

				_isTrailingTakeProfit.Value = value;
			}
		}

		private readonly StrategyParam<TimeSpan> _stopLossTimeOut;

		/// <summary>
		/// Ограничение по времени для <see cref="StopLossStrategy"/>.
		/// Если защита не сработает к этому времени, то позиция будет закрыта по рынку.
		/// По-умолчанию выключено.
		/// </summary>
		public TimeSpan StopLossTimeOut
		{
			get { return _stopLossTimeOut.Value; }
			set
			{
				if (value < TimeSpan.Zero)
					throw new ArgumentOutOfRangeException("value", value, "Ограничение по времени не может быть отрицательным.");

				_stopLossTimeOut.Value = value;
			}
		}

		private readonly StrategyParam<TimeSpan> _takeProfitTimeOut;

		/// <summary>
		/// Ограничение по времени для <see cref="TakeProfitStrategy"/>.
		/// Если защита не сработает к этому времени, то позиция будет закрыта по рынку.
		/// По-умолчанию выключено.
		/// </summary>
		public TimeSpan TakeProfitTimeOut
		{
			get { return _takeProfitTimeOut.Value; }
			set
			{
				if (value < TimeSpan.Zero)
					throw new ArgumentOutOfRangeException("value", value, "Ограничение по времени не может быть отрицательным.");

				_takeProfitTimeOut.Value = value;
			}
		}

		/// <summary>
		/// Получить или установить первоначальную позицию для инструмента.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <returns>Позиция.</returns>
		public decimal this[Security security]
		{
			get { return GetPositionManager(security).Position; }
			set
			{
				var prevPos = this[security];

				if (prevPos != value)
				{
					// закрытие или переворот позы
					if (prevPos.Sign() != value.Sign())
					{
						CloseProtective(security);

						// переворот
						if (value != 0)
						{
							IncreasePosition(value);
						}
					}
					else
					{
						var diffVolume = value.Abs() - prevPos.Abs();

						if (diffVolume == 0)
							return;

						// увеличение позы
						if (diffVolume > 0)
						{
							IncreasePosition(diffVolume * value.Sign());
						}
						// уменьшение позы
						else
						{
							DecreasePosition(security, diffVolume.Abs());
						}
					}

					GetPositionManager(security).Position = value;
				}
			}
		}

		private IMarketRule _myTradesRule;
		private Strategy _myTradesStrategy;

		/// <summary>
		/// Стратегия, новые сделки которой автоматически транслируются в <see cref="ProcessNewMyTrades"/>.
		/// </summary>
		public Strategy MyTradesStrategy
		{
			get { return _myTradesStrategy; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");

				if (_myTradesStrategy == value)
					return;

				if (_myTradesRule != null)
					Rules.Remove(_myTradesRule);

				_myTradesStrategy = value;

				if (ProcessState == ProcessStates.Started)
					ApplyRule();
			}
		}

		private void ApplyRule()
		{
			_myTradesRule = MyTradesStrategy
					.WhenNewMyTrades()
					.Do(ProcessNewMyTrades)
					.Apply(this);
		}

		/// <summary>
		/// Метод вызывается тогда, когда вызвался метод <see cref="Strategy.Start"/>, и состояние <see cref="Strategy.ProcessState"/> перешло в значение <see cref="ProcessStates.Started"/>.
		/// </summary>
		protected override void OnStarted()
		{
			base.OnStarted();

			if (MyTradesStrategy != null)
				ApplyRule();
		}

		/// <summary>
		/// Защитить позицию, которая была обновлена через <see cref="this[Security]"/>.
		/// </summary>
		/// <param name="position">Позиция.</param>
		/// <returns>Защитная стратегия. Если будет возвращено null, значит защита позиции игнорируется.</returns>
		protected virtual IProtectiveStrategy Protect(decimal position)
		{
			return null;
		}

		/// <summary>
		/// Защитить сделки.
		/// </summary>
		/// <param name="trades">Сделки. По ключу указана защищаемая сделка, по значению указан объем, который необходимо защитить.</param>
		/// <returns>Защитные стратегии.</returns>
		protected virtual IEnumerable<IProtectiveStrategy> Protect(IDictionary<MyTrade, decimal> trades)
		{
			return trades.Select(p =>
			{
				IProtectiveStrategy protectiveStrategy;

				var trade = p.Key;

				var takeProfit = TakeProfitLevel == (Unit)0 ? null : new TakeProfitStrategy(trade, TakeProfitLevel)
				{
					IsTrailing = IsTrailingTakeProfit,
					TimeOut = TakeProfitTimeOut,
				};

				var stopLoss = StopLossLevel == (Unit)0 ? null : new StopLossStrategy(trade, StopLossLevel)
				{
					IsTrailing = IsTrailingStopLoss,
					ActivationOffset = StopLossActivationOffset,
					TimeOut = StopLossTimeOut,
				};

				if (takeProfit != null && stopLoss != null)
					protectiveStrategy = new TakeProfitStopLossStrategy(takeProfit, stopLoss);
				else if (takeProfit != null)
					protectiveStrategy = takeProfit;
				else
					protectiveStrategy = stopLoss;

				if (protectiveStrategy != null)
				{
					((Strategy)protectiveStrategy).DisposeOnStop = true;
					protectiveStrategy.ProtectiveVolume = p.Value;
				}

				return protectiveStrategy;
			}).Where(s => s != null);
		}

		/// <summary>
		/// Отсортировать защитные стратегии для указания наиболее худшей и лучшей по рыночным ценам
		/// (при частичной закрытие позиции в первую очередь снимаются наиболее худшие).
		/// </summary>
		/// <param name="strategies">Защитные стратегии в неотсортированном порядке.</param>
		/// <returns>Защитные стратегии в отсортированном порядке.</returns>
		protected virtual IEnumerable<IGrouping<MyTrade, IProtectiveStrategy>> Sort(IEnumerable<IGrouping<MyTrade, IProtectiveStrategy>> strategies)
		{
			var dir = strategies.First().Key.Order.Direction;
			return dir == OrderDirections.Buy ? strategies.OrderBy(s => s.Key.Trade.Price) : strategies.OrderByDescending(s => s.Key.Trade.Price);
		}

		/// <summary>
		/// Получить из всех сделок те, которые необходимо защитить.
		/// </summary>
		/// <param name="trades">Новые сделки.</param>
		/// <param name="maxVolume">Максимальный объем, который необъодимо защитить.</param>
		/// <returns>Сделки, которые необходимо защитить.</returns>
		protected virtual IDictionary<MyTrade, decimal> GetProtectiveTrades(IEnumerable<MyTrade> trades, decimal maxVolume)
		{
			if (trades == null)
				throw new ArgumentNullException("trades");

			if (maxVolume <= 0)
				throw new ArgumentOutOfRangeException("maxVolume", maxVolume, "Максимальный объем не может быть меньше или равен нулю.");

			var dir = trades.First().Order.Direction;

			trades = dir == OrderDirections.Buy ? trades.OrderBy(t => t.Trade.Price) : trades.OrderByDescending(t => t.Trade.Price);

			var retVal = new Dictionary<MyTrade, decimal>();

			foreach (var trade in trades)
			{
				var volume = trade.Trade.Volume.Min(maxVolume);

				retVal.Add(trade, volume);

				maxVolume -= volume;

				if (maxVolume <= 0)
					break;
			}

			return retVal;
		}

		/// <summary>
		/// Обработать сделки для коррекции объема защитных стратегий.
		/// </summary>
		/// <param name="trades">Сделки.</param>
		public void ProcessNewMyTrades(IEnumerable<MyTrade> trades)
		{
			this.AddInfoLog("Получено {0} сделок.", trades.Count());

			foreach (var group in trades.GroupBy(t => t.Order.Security).ToArray())
			{
				var security = group.Key;

				var manager = GetPositionManager(security);

				var prevPos = manager.Position;

				group.ForEach(t => manager.ProcessMyTrade(t));

				this.AddInfoLog("Для инструмента {0} пред позиция {1}, новая {2}.", security.Id, prevPos, manager.Position);

				if (prevPos == 0)
				{
					Protect(manager, group.ToDictionary(t => t, t => t.Trade.Volume));
					return;
				}

				// закрытие или переворот позы
				if (prevPos.Sign() != manager.Position.Sign())
				{
					// закрываем старые стопы
					CloseProtective(security);

					// переворот позы
					if (manager.Position != 0)
					{
						this.AddInfoLog("Переворот позиции.");
						Protect(manager, GetProtectiveTrades(group, manager.Position.Abs()));
					}
				}
				else
				{
					var diffVolume = manager.Position.Abs() - prevPos.Abs();

					if (diffVolume == 0)
						return;

					// увеличение позы
					if (diffVolume > 0)
					{
						this.AddInfoLog("Увеличение позиции на {0}.", diffVolume);
						Protect(manager, group.ToDictionary(t => t, t => t.Trade.Volume));
					}
					// уменьшение позы
					else
					{
						DecreasePosition(security, diffVolume.Abs());
					}
				}
			}
		}

		private void CloseProtective(Security security)
		{
			foreach (var strategies in ChildStrategies
						.Where(s => s.Security == security)
						.OfType<IProtectiveStrategy>()
						.GroupBy(s => s.Trade).ToArray())
			{
				strategies.OfType<Strategy>().ForEach(s => s.Stop());
			}
		}

		private void IncreasePosition(decimal position)
		{
			var strategy = Protect(position);

			if (strategy != null)
			{
				this.AddInfoLog("Изменение позиции на {0}.", position);
				ChildStrategies.Add((Strategy)strategy);
			}
		}

		private void DecreasePosition(Security security, decimal diffVolume)
		{
			this.AddInfoLog("Уменьшение позиции на {0}.", diffVolume);

			var groups = ChildStrategies
				.Where(s => s.Security == security)
				.OfType<IProtectiveStrategy>()
				.GroupBy(s => s.Trade).ToArray();

			if (groups.Length == 0)
			{
				this.AddWarningLog("Стопы не были найдены.");
				return;
			}

			foreach (var strategies in Sort(groups))
			{
				this.AddInfoLog("Уменьшение стопов на {0}.", diffVolume);

				diffVolume = ChangeVolume(strategies, diffVolume);

				if (diffVolume <= 0)
					break;
			}
		}

		private IPositionManager GetPositionManager(Security security)
		{
			return _positionManagers.SafeAdd(security, key => new PositionManager(false));
		}

		private void Protect(IPositionManager positionManager, IDictionary<MyTrade, decimal> trades)
		{
			var strategies = Protect(trades).Cast<Strategy>().ToArray();

			if (strategies.Length == 0)
			{
				this.AddWarningLog("Нет защитных стратегий.");
			}

			strategies.ForEach(s =>
			{
				s.NewMyTrades += protectiveTrades =>
				{
					this.AddInfoLog("Получены {0} сделок от защитной стратегии {1}.", protectiveTrades.Count(), s.Name);

					foreach (var trade in protectiveTrades)
					{
						var prevPos = positionManager.Position;

						positionManager.ProcessMyTrade(trade);

						this.AddInfoLog("Для инструмента {0} пред позиция {1}, новая {2}, сделка {3}.", trade.Trade.Security.Id, prevPos, positionManager.Position, trade);
					}
				};
			});

			ChildStrategies.AddRange(strategies);
		}

		private decimal ChangeVolume(IEnumerable<IProtectiveStrategy> strategies, decimal removableVolume)
		{
			if (removableVolume <= 0)
				throw new ArgumentOutOfRangeException();

			// старый котируемый объем
			var volume = strategies.First().ProtectiveVolume;

			// новый котируемый объем
			var newVolume = Math.Max(0, volume - removableVolume);

			if (newVolume == 0)
			{
				this.AddInfoLog("Остановка защитных стратегий.");

				strategies.Cast<Strategy>().ForEach(s => s.Stop());
			}
			else
			{
				foreach (var strategy in strategies)
					strategy.ProtectiveVolume = newVolume;
			}

			// оставшийся объем для обновления стратегий
			return removableVolume - volume;
		}
	}
}